import sequtils, sets, hashes, strformat

type
  Reactor = ref object
    cells: seq[Cell]
    pendingCallbacks: HashSet[Callback]

  CellType = enum 
    ctInput, ctCompute

  Cell* = ref object
    value: int
    reactor: Reactor
    case type: CellType
    of ctCompute:
      action: proc(vals: seq[int]): int
      dependsOn: seq[Cell]
      callbacks: HashSet[Callback]
    of ctInput:
      discard
    
  Callback = ref object
    action: proc(val: int)
    cell: Cell

proc hash(callback: Callback): Hash
proc values(a: openArray[Cell]): seq[int]
proc stabilize(cell: Cell, newVal: int)
proc `$`(cell: Cell): string
proc `$`(callback: Callback): string

using
  r: Reactor

proc newReactor*(): Reactor =
  Reactor(cells: @[], pendingCallbacks: initHashSet[Callback]())

proc update(r; oldState: openArray[int]) =
  for (old, new) in zip(oldState, r.cells):
    if old != new.value and new.type == ctCompute:
      r.pendingCallbacks.incl new.callbacks
  
  for callback in r.pendingCallbacks:
    callback.action(callback.cell.value)
  
  r.pendingCallbacks.clear

proc createInput*(r; n: int): Cell =
  result = Cell(type: ctInput, value: n, reactor: r)
  r.cells.add(result)

proc createCompute*(r; cells: seq[Cell],
                    action: proc(vals: seq[int]): int): Cell =
  result = Cell(
    type: ctCompute,
    value: action(cells.values),
    reactor: r,
    action: action,
    dependsOn: cells,
    callbacks: initHashSet[Callback]())
  r.cells.add(result)

using 
  cell: Cell

proc addCallback*(cell; callback: proc(val: int)): Callback =
  result = Callback(action: callback, cell: cell)
  cell.callbacks.incl result

proc removeCallback*(cell; callback: Callback) =
  cell.callbacks.excl callback

proc value*(cell): int = 
  cell.value

proc `value=`*(cell; newVal: int) =
  let oldState = cell.reactor.cells.values
  stabilize(cell, newVal)
  cell.reactor.update(oldState)

proc stabilize(cell; newVal: int) =
  cell.value = newVal
  for c in cell.reactor.cells:
    if c.type == ctCompute and cell in c.dependsOn:
      stabilize(c, c.action(c.dependsOn.values))

proc hash(callback: Callback): Hash =
  result = !$ hash(callback.action)

proc values(a: openArray[Cell]): seq[int] =
  a.map value

proc `$`(cell: Cell): string =
  &"#Cell(type: {cell.type}, value: {cell.value})"

proc `$`(callback: Callback): string =
  &"#Callback(cell: {callback.cell})"